// Host defines
#define NUM_THREADS 8
#define STR_tamVectorPoblacion 50

// Includes
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <limits.h>
#include <iostream>
#include <string>
#include <ctime>
#include <cstdlib>

//Incluyo la clase Mersenne-Twister para la generación de números aleatorios en C++
#include "mt.h"

//Incluyo la clase Random para la generación de números aleatorios en CUDA
#include "Random.cu"

//Semilla aleatoria a utilizar.
#define SEMILLA (time(NULL))

using namespace std;
//using std::string;

int totalGenes;
int totalIndividuos;
int cantidadPoblaciones = 3;
int cantCromosomasPoblacion = 10;
int cantGenesEnCromosoma = 8;

__device__ int cantidadPoblacionesDevice = 3;
__device__ int cantCromosomasPoblacionDevice = 10;
__device__ int cantGenesEnCromosomaDevice = 8;
__device__ int totalGenesEnPoblacionDevice = 80;

// Declaracion de las funciones
__global__ void inicializarPoblacion(int* poblacion, int* fitnessActualDevice, struct Random randomGPU, int cantCromosomasPoblacion);
__global__ void main_kernel(int* poblacionActualDevice, int* nuevaPoblacionDevice, int* fitnessActualDevice, int* nuevoFitnessDevice, struct Random r);
__device__ void asignarFitness(int* poblacion, int* vectorFitness);
__device__ int seleccionPorTorneo(int* vectorFitness, struct Random randomGPU);
__device__ void cruzamientoCromosoma1Punto(int* poblacionActual, int* nuevaPoblacion, int posicionCromosoma1, int posicionCromosoma2, int probabilidad, struct Random randomGPU);
__device__ void mutacion(int* poblacion, int probabilidad, struct Random randomGPU);
__device__ int calcularFitness(int* poblacion, int posicion);

// Implementacion de las funciones

__global__ void inicializarPoblacion(int* poblacion, int* fitnessActualDevice, struct Random randomGPU, int cantCromosomasPoblacion){

		//Obtengo posicion a escribir.
		int posicionCromosoma = threadIdx.y * cantGenesEnCromosomaDevice * 2 + blockIdx.y * totalGenesEnPoblacionDevice;
		int posicionFitness = threadIdx.y * 2 + blockIdx.y * cantCromosomasPoblacionDevice;
		for(int i = 0; i<cantGenesEnCromosomaDevice * 2; i++){
			//Genero valor aleatorio.
			unsigned int r = (getRandomInt(randomGPU, posicionCromosoma)% 10) + 1;
			if (r <= 5) {
				poblacion[posicionCromosoma+i] = 0;
			}
			else {
				poblacion[posicionCromosoma+i] = 1;
			}
		}

		fitnessActualDevice[posicionFitness] = calcularFitness(poblacion, posicionCromosoma);
		fitnessActualDevice[posicionFitness + 1] = calcularFitness(poblacion, posicionCromosoma + cantGenesEnCromosomaDevice);

}

// GPU Kernels declarations
__global__ void main_kernel(int* poblacionActualDevice, int* nuevaPoblacionDevice, int* fitnessActualDevice, int* nuevoFitnessDevice, struct Random r){
	//genero numero aleatorio entre 0 y cant individuos por poblacion
	int probabilidadCruzamiento = 80;
	int probabilidadMutacion = 80;
	int posicionCromosoma1 = seleccionPorTorneo(fitnessActualDevice, r);
	int posicionCromosoma2 = seleccionPorTorneo(fitnessActualDevice, r);
	cruzamientoCromosoma1Punto(poblacionActualDevice, nuevaPoblacionDevice, posicionCromosoma1, posicionCromosoma2, probabilidadCruzamiento, r);
	mutacion(nuevaPoblacionDevice, probabilidadMutacion, r);
	asignarFitness(nuevaPoblacionDevice, nuevoFitnessDevice);
}

__device__ void asignarFitness(int* nuevaPoblacion, int* nuevoVectorFitness){
	//Obtengo posicion del primer cromosoma.
	int posicionCromosoma = threadIdx.y * cantGenesEnCromosomaDevice * 2 + blockIdx.y * totalGenesEnPoblacionDevice;
	int posicionFitness = threadIdx.y * 2 + blockIdx.y * cantCromosomasPoblacionDevice;
	nuevoVectorFitness[posicionFitness] = calcularFitness(nuevaPoblacion, posicionCromosoma);
	nuevoVectorFitness[posicionFitness + 1] = calcularFitness(nuevaPoblacion, posicionCromosoma + cantGenesEnCromosomaDevice);
}

//Seleccion por torneo
//selecciona 2 individuos al azar y devuelve la posicion del mejor
__device__ int seleccionPorTorneo(int* vectorFitness, struct Random randomGPU){

	unsigned int r = getRandomInt(randomGPU, threadIdx.y)% cantCromosomasPoblacionDevice;
	int posicionFitnessAleatoria1 = blockIdx.y * cantCromosomasPoblacionDevice + r;
	int posicionCromosomaAleatoria1 = blockIdx.y * cantCromosomasPoblacionDevice * cantGenesEnCromosomaDevice + r * cantGenesEnCromosomaDevice;
	r = getRandomInt(randomGPU, threadIdx.y)% cantCromosomasPoblacionDevice;
	int posicionFitnessAleatoria2 = blockIdx.y * cantCromosomasPoblacionDevice + r;
	int posicionCromosomaAleatoria2 = blockIdx.y * cantCromosomasPoblacionDevice * cantGenesEnCromosomaDevice + r * cantGenesEnCromosomaDevice;

	if (vectorFitness[posicionFitnessAleatoria1] > vectorFitness[posicionFitnessAleatoria2])
		return posicionCromosomaAleatoria1;
	return posicionCromosomaAleatoria2;
}

__device__ void cruzamientoCromosoma1Punto(int* poblacionActual, int* nuevaPoblacion, int posicionCromosoma1, int posicionCromosoma2, int probabilidad, struct Random randomGPU){
	int posicionNuevoCromosoma1 = threadIdx.y * cantGenesEnCromosomaDevice * 2 + blockIdx.y * totalGenesEnPoblacionDevice;
	int posicionNuevoCromosoma2 = posicionNuevoCromosoma1 + cantGenesEnCromosomaDevice;
	if((getRandomInt(randomGPU, posicionNuevoCromosoma1) % 101) >= probabilidad ){
		unsigned int r = getRandomInt(randomGPU, posicionNuevoCromosoma1) % cantGenesEnCromosomaDevice;
		for(int i=0; i<cantGenesEnCromosomaDevice; i++){
			if(i<=r){
				nuevaPoblacion[posicionNuevoCromosoma1+i] = poblacionActual[posicionCromosoma1 + i];
				nuevaPoblacion[posicionNuevoCromosoma2+i] = poblacionActual[posicionCromosoma2 + i];
			}else{
				nuevaPoblacion[posicionNuevoCromosoma1+i] = poblacionActual[posicionCromosoma2 + i];
				nuevaPoblacion[posicionNuevoCromosoma2+i] = poblacionActual[posicionCromosoma1 + i];
			}
		}
	}else{
		for(int i=0; i<cantGenesEnCromosomaDevice; i++){
			nuevaPoblacion[posicionNuevoCromosoma1+i] = poblacionActual[posicionCromosoma1 + i];
			nuevaPoblacion[posicionNuevoCromosoma2+i] = poblacionActual[posicionCromosoma2 + i];
		}
	}
}

//muta con probabilidad los 2 cromosomas asociados al hilo
__device__ void mutacion(int* poblacion, int probabilidad, struct Random randomGPU){
	unsigned int r;
	int posicionNuevoCromosoma1 = threadIdx.y * cantGenesEnCromosomaDevice * 2 + blockIdx.y * cantCromosomasPoblacionDevice;
	int posicionNuevoCromosoma2 = posicionNuevoCromosoma1 + cantGenesEnCromosomaDevice;
	if((getRandomInt(randomGPU, posicionNuevoCromosoma1) % 101) >= probabilidad ){
		r = (getRandomInt(randomGPU, posicionNuevoCromosoma1) % cantGenesEnCromosomaDevice);
		if(poblacion[posicionNuevoCromosoma1+r] == 0)
			poblacion[posicionNuevoCromosoma1+r] = 1;
		else
			poblacion[posicionNuevoCromosoma1+r] = 0;
	}
	if((getRandomInt(randomGPU, posicionNuevoCromosoma2) % 101) >= probabilidad ){
		r = (getRandomInt(randomGPU, posicionNuevoCromosoma2) % cantGenesEnCromosomaDevice);
		if(poblacion[posicionNuevoCromosoma2+r] == 0)
			poblacion[posicionNuevoCromosoma2+r] = 1;
		else
			poblacion[posicionNuevoCromosoma2+r] = 0;

	}
}

__device__ int calcularFitness(int* poblacion, int posicion) {
	int fitness = 0;
	float contador = 7;
	for(int i=0; i<cantGenesEnCromosomaDevice; i++){
	  fitness = fitness + poblacion[posicion + i] * __powf(2.0,contador);
	  contador--;
	}
	return fitness;
}

//////////// FUNCIONES EN EL HOST

//__host__ puede ponerse o no


//////////////////////
// Program main
//////////////////////
int main( int argc, char** argv)
{

	int probabilidadMutacion = 80;
	int probabilidadCruzamiento = 20;

	totalGenes = cantidadPoblaciones * cantCromosomasPoblacion * cantGenesEnCromosoma;
	totalIndividuos = cantCromosomasPoblacion*cantidadPoblaciones;
	int tamVectorPoblacion = sizeof(int)*totalGenes;
	int tamVectorFitness = sizeof(int)*totalIndividuos;

	static int *poblacionActualHost = (int *) malloc(tamVectorPoblacion);
	static int *nuevaPoblacionHost = (int *) malloc(tamVectorPoblacion);
	static int *fitnessActualHost = (int *) malloc(tamVectorFitness);
	static int *nuevoFitnessHost = (int *) malloc(tamVectorFitness);

	int *poblacionActualDevice;
	int *nuevaPoblacionDevice;
	int *fitnessActualDevice;
	int *nuevoFitnessDevice;

	// alocar en el device
	cudaMalloc(&poblacionActualDevice, tamVectorPoblacion);
	cudaMalloc(&nuevaPoblacionDevice, tamVectorPoblacion);
	cudaMalloc(&fitnessActualDevice, tamVectorFitness);
	cudaMalloc(&nuevoFitnessDevice, tamVectorFitness);

	//Semilla randomica.
	srand48(SEMILLA);

	//Inicio numeros aleatorios.
	struct Random r = initRandom(1*cantCromosomasPoblacion/2);

	//cuantos bloques? (1)
	dim3 tamGrid(1, cantidadPoblaciones); //Grid dimensión
	//cuantos hilos por bloque? (N) o sea tengo un bloque y n hilos (un hilo por columna)
	dim3 tamBlock(1,cantCromosomasPoblacion/2,1); //Block dimensión

	//inicializacion de la poblacion en GPU
	inicializarPoblacion<<<tamGrid, tamBlock>>>(poblacionActualDevice, fitnessActualDevice, r, cantCromosomasPoblacion);

	// Traer resultado
	cudaMemcpy (fitnessActualHost, fitnessActualDevice, tamVectorFitness, cudaMemcpyDeviceToHost);
	cudaMemcpy (poblacionActualHost, poblacionActualDevice, tamVectorPoblacion, cudaMemcpyDeviceToHost);

	for (int i = 0; i<totalGenes; i++){
		if(i%cantGenesEnCromosoma == 0){
			cout << "\n";

		}
		printf("%i ", poblacionActualHost[i]);
	}
	cout << "\n\n\nFITNESS\n";

	for (int i = 0; i<cantCromosomasPoblacion * cantidadPoblaciones; i++){
		printf("%i ", fitnessActualHost[i]);
	}
	cout << "\n\n\n\n";


	int iteracion = 0;
	int cont = 0;
	//los individuos de la poblacion actual se van intercambiando en el reemplazo
	while(cont < 1){

		//si la iteracion es par invoco a GPU con poblacionActual, nuevaPoblacion
		if ((iteracion % 2) == 0) {
			// seleccionamos, cruzamos y mutamos en GPU
			main_kernel<<<tamGrid, tamBlock>>>(poblacionActualDevice, nuevaPoblacionDevice, fitnessActualDevice, nuevoFitnessDevice, r);
		}
		//sino invoco a GPU con nuevaPoblacion, poblacionActual
		else {
			// seleccionamos, cruzamos y mutamos en GPU
			main_kernel<<<tamGrid, tamBlock>>>(nuevaPoblacionDevice, poblacionActualDevice, nuevoFitnessDevice, fitnessActualDevice, r);
		}

		//mejorFitness = obtenerMejorFitness(poblacionActual,mejorCromosoma);
		cont++;
	}


	// Traer resultado
	cudaMemcpy (nuevoFitnessHost, nuevoFitnessDevice, tamVectorFitness, cudaMemcpyDeviceToHost);
	cudaMemcpy (nuevaPoblacionHost, nuevaPoblacionDevice, tamVectorPoblacion, cudaMemcpyDeviceToHost);

	// liberar memoria de las matrices en el device
	cudaFree(fitnessActualDevice);
	cudaFree(poblacionActualDevice);


	for (int i = 0; i<totalGenes; i++){
		if(i%cantGenesEnCromosoma == 0){
			cout << "\n";

		}
		printf("%i ", nuevaPoblacionHost[i]);
	}
	cout << "\n\n\nFITNESS\n";

	for (int i = 0; i<cantCromosomasPoblacion * cantidadPoblaciones; i++){
		printf("%i ", nuevoFitnessHost[i]);
	}
	cout << "\n";

}
